package net.nightwhistler.htmlspanner.css;

import android.graphics.Color;
import android.util.Log;
import com.osbcp.cssparser.PropertyValue;
import com.osbcp.cssparser.Rule;
import com.osbcp.cssparser.Selector;
import net.nightwhistler.htmlspanner.FontFamily;
import net.nightwhistler.htmlspanner.HtmlSpanner;
import net.nightwhistler.htmlspanner.style.Style;
import net.nightwhistler.htmlspanner.style.StyleValue;
import org.htmlcleaner.TagNode;

import java.util.ArrayList;
import java.util.List;

/**
 * Compiler for CSS Rules.
 * 
 * The compiler takes the raw parsed form (a Rule) of a CSS rule and transforms
 * it into an executable CompiledRule where all the parsing of values has
 * already been done.
 * 
 * 
 */
public class CSSCompiler {

	public interface StyleUpdater {
		Style updateStyle(Style style, HtmlSpanner spanner);
	}

	public interface TagNodeMatcher {
		boolean matches(TagNode tagNode);
	}

	public static CompiledRule compile(Rule rule, HtmlSpanner spanner) {

		Log.d("CSSCompiler", "Compiling rule " + rule);

		List<List<TagNodeMatcher>> matchers = new ArrayList<>();
		List<StyleUpdater> styleUpdaters = new ArrayList<>();

		for (Selector selector : rule.getSelectors()) {
			List<CSSCompiler.TagNodeMatcher> selMatchers = CSSCompiler
					.createMatchersFromSelector(selector);
			matchers.add(selMatchers);
		}

		Style blank = new Style();

		for (PropertyValue propertyValue : rule.getPropertyValues()) {
			CSSCompiler.StyleUpdater updater = CSSCompiler.getStyleUpdater(
					propertyValue.getProperty(), propertyValue.getValue());

			if (updater != null) {
				styleUpdaters.add(updater);
				blank = updater.updateStyle(blank, spanner);
			}
		}

		Log.d("CSSCompiler", "Compiled rule: " + blank);

		String asText = rule.toString();

		return new CompiledRule(spanner, matchers, styleUpdaters, asText);
	}

	public static Integer parseCSSColor(String colorString) {

		// Check for CSS short-hand notation: #0fc -> #00ffcc
		if (colorString.length() == 4 && colorString.startsWith("#")) {
			StringBuilder builder = new StringBuilder("#");
			for (int i = 1; i < colorString.length(); i++) {
				// Duplicate each char
				builder.append(colorString.charAt(i));
				builder.append(colorString.charAt(i));
			}

			colorString = builder.toString();
		}

		return Color.parseColor(colorString);
	}

	public static List<TagNodeMatcher> createMatchersFromSelector(
			Selector selector) {
		List<TagNodeMatcher> matchers = new ArrayList<>();

		String selectorString = selector.toString();

		String[] parts = selectorString.split("\\s");

		// Create a reversed matcher list
		for (int i = parts.length - 1; i >= 0; i--) {
			matchers.add(createMatcherFromPart(parts[i]));
		}

		return matchers;
	}

	private static TagNodeMatcher createMatcherFromPart(String selectorPart) {

		// Match by class
		if (selectorPart.indexOf('.') != -1) {
			return new ClassMatcher(selectorPart);
		}

		if (selectorPart.startsWith("#")) {
			return new IdMatcher(selectorPart);
		}

		return new TagNameMatcher(selectorPart);
	}

	private static class ClassMatcher implements TagNodeMatcher {

		private String tagName;
		private String className;

		private ClassMatcher(String selectorString) {

			String[] elements = selectorString.split("\\.");

			if (elements.length == 2) {
				tagName = elements[0];
				className = elements[1];
			}
		}

		@Override
		public boolean matches(TagNode tagNode) {

			if (tagNode == null) {
				return false;
			}

			// If a tag name is given it should match
			if (tagName != null && tagName.length() > 0
					&& !tagName.equals(tagNode.getName())) {
				return false;
			}

			String classAttribute = tagNode.getAttributeByName("class");
			return classAttribute != null && classAttribute.equals(className);
		}
	}

	private static class TagNameMatcher implements TagNodeMatcher {
		private String tagName;

		private TagNameMatcher(String selectorString) {
			this.tagName = selectorString.trim();
		}

		@Override
		public boolean matches(TagNode tagNode) {
			return tagNode != null
					&& tagName.equalsIgnoreCase(tagNode.getName());
		}
	}

	private static class IdMatcher implements TagNodeMatcher {
		private String id;

		private IdMatcher(String selectorString) {
			id = selectorString.substring(1);
		}

		@Override
		public boolean matches(TagNode tagNode) {

			if (tagNode == null) {
				return false;
			}

			String idAttribute = tagNode.getAttributeByName("id");
			return idAttribute != null && idAttribute.equals(id);
		}
	}

	public static StyleUpdater getStyleUpdater(final String key,
			final String value) {

		if ("color".equals(key)) {
			try {
				final Integer color = parseCSSColor(value);
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						Log.d("CSSCompiler", "Applying style " + key + ": "
								+ value);
						return style.setColor(color);
					}
				};
			} catch (IllegalArgumentException ia) {
				Log.e("CSSCompiler", "Can't parse colour definition: " + value);
				return null;
			}
		}

		if ("background-color".equals(key)) {
			try {
				final Integer color = parseCSSColor(value);
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						Log.d("CSSCompiler", "Applying style " + key + ": "
								+ value);
						return style.setBackgroundColor(color);
					}
				};
			} catch (IllegalArgumentException ia) {
				Log.e("CSSCompiler", "Can't parse colour definition: " + value);
				return null;
			}
		}

		if ("align".equals(key) || "text-align".equals(key)) {
			try {
				final Style.TextAlignment alignment = Style.TextAlignment
						.valueOf(value.toUpperCase());
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						Log.d("CSSCompiler", "Applying style " + key + ": "
								+ value);
						return style.setTextAlignment(alignment);
					}
				};

			} catch (IllegalArgumentException i) {
				Log.e("CSSCompiler", "Can't parse alignment: " + value);
				return null;
			}
		}

		if ("font-weight".equals(key)) {

			try {
				final Style.FontWeight weight = Style.FontWeight.valueOf(value
						.toUpperCase());

				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						Log.d("CSSCompiler", "Applying style " + key + ": "
								+ value);
						return style.setFontWeight(weight);
					}
				};

			} catch (IllegalArgumentException i) {
				Log.e("CSSCompiler", "Can't parse font-weight: " + value);
				return null;
			}
		}

		if ("font-style".equals(key)) {
			try {
				final Style.FontStyle fontStyle = Style.FontStyle.valueOf(value
						.toUpperCase());
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						Log.d("CSSCompiler", "Applying style " + key + ": "
								+ value);
						return style.setFontStyle(fontStyle);
					}
				};
			} catch (IllegalArgumentException i) {
				Log.e("CSSCompiler", "Can't parse font-style: " + value);
				return null;
			}
		}

		if ("font-family".equals(key)) {
			return new StyleUpdater() {
				@Override
				public Style updateStyle(Style style, HtmlSpanner spanner) {
					Log.d("CSSCompiler", "Applying style " + key + ": " + value);

					FontFamily family = spanner.getFont(value);

					Log.d("CSSCompiler", "Got font " + family);

					return style.setFontFamily(family);
				}
			};

		}

		if ("font-size".equals(key)) {

			final StyleValue styleValue = StyleValue.parse(value);

			if (styleValue != null) {

				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						Log.d("CSSCompiler", "Applying style " + key + ": "
								+ value);
						return style.setFontSize(styleValue);
					}
				};

			} else {

				// Fonts have an extra legacy format where you just specify a
				// plain number.
				try {
					final Float number = translateFontSize(Integer
							.parseInt(value));
					return new StyleUpdater() {
						@Override
						public Style updateStyle(Style style,
								HtmlSpanner spanner) {
							Log.d("CSSCompiler", "Applying style " + key + ": "
									+ value);
							return style.setFontSize(new StyleValue(number,
									StyleValue.Unit.EM));
						}
					};
				} catch (NumberFormatException nfe) {
					Log.e("CSSCompiler", "Can't parse font-size: " + value);
					return null;
				}
			}
		}

		if ("margin-bottom".equals(key)) {

			final StyleValue styleValue = StyleValue.parse(value);

			if (styleValue != null) {
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						return style.setMarginBottom(styleValue);
					}
				};
			}
		}

		if ("margin-top".equals(key)) {

			final StyleValue styleValue = StyleValue.parse(value);

			if (styleValue != null) {
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						return style.setMarginTop(styleValue);
					}
				};
			}
		}

		if ("margin-left".equals(key)) {

			final StyleValue styleValue = StyleValue.parse(value);

			if (styleValue != null) {
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						return style.setMarginLeft(styleValue);
					}
				};
			}
		}

		if ("margin-right".equals(key)) {

			final StyleValue styleValue = StyleValue.parse(value);

			if (styleValue != null) {
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						return style.setMarginRight(styleValue);
					}
				};
			}
		}

		if ("margin".equals(key)) {
			return parseMargin(value);
		}

		if ("text-indent".equals(key)) {
			final StyleValue styleValue = StyleValue.parse(value);

			if (styleValue != null) {
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						return style.setTextIndent(styleValue);
					}
				};
			}
		}

		if ("display".equals(key)) {
			try {
				final Style.DisplayStyle displayStyle = Style.DisplayStyle
						.valueOf(value.toUpperCase());
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						return style.setDisplayStyle(displayStyle);
					}
				};
			} catch (IllegalArgumentException ia) {
				Log.e("CSSCompiler", "Can't parse display-value: " + value);
				return null;
			}
		}

		if ("border-style".equals(key)) {
			try {
				final Style.BorderStyle borderStyle = Style.BorderStyle
						.valueOf(value.toUpperCase());
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						return style.setBorderStyle(borderStyle);
					}
				};
			} catch (IllegalArgumentException ia) {
				Log.e("CSSCompiler", "Could not parse border-style " + value);
				return null;
			}
		}

		if ("border-color".equals(key)) {
			try {
				final Integer borderColor = parseCSSColor(value);
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						return style.setBorderColor(borderColor);
					}
				};
			} catch (IllegalArgumentException ia) {
				Log.e("CSSCompiler", "Could not parse border-color " + value);
				return null;
			}
		}

		if ("border-width".equals(key)) {

			final StyleValue borderWidth = StyleValue.parse(value);
			if (borderWidth != null) {
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						return style.setBorderWidth(borderWidth);
					}
				};
			} else {
				Log.e("CSSCompiler", "Could not parse border-width " + value);
				return null;
			}
		}

		if ("border-top-width".equals(key)) {

			final StyleValue borderTopWidth = StyleValue.parse(value);
			if (borderTopWidth != null) {
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						return style.setBorderTopWidth(borderTopWidth);
					}
				};
			} else {
				Log.e("CSSCompiler", "Could not parse border-top-width "
						+ value);
				return null;
			}
		}

		if ("border-left".equals(key)) {

			final Boolean hasBorderLeft = parseHasBorder(value);
			if (hasBorderLeft != null) {
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						return style.setHasBorderLeft(hasBorderLeft);
					}
				};
			} else {
				Log.e("CSSCompiler", "Could not parse border-left " + value);
				return null;
			}
		}
		
		if ("border-right".equals(key)) {

			final Boolean hasBorderRight = parseHasBorder(value);
			if (hasBorderRight != null) {
				return new StyleUpdater() {
					@Override
					public Style updateStyle(Style style, HtmlSpanner spanner) {
						return style.setHasBorderRight(hasBorderRight);
					}
				};
			} else {
				Log.e("CSSCompiler", "Could not parse border-right " + value);
				return null;
			}
		}

		if ("border".equals(key)) {
			return parseBorder(value);
		}

		Log.d("CSSCompiler", "Don't understand CSS property '" + key
				+ "'. Ignoring it.");
		return null;
	}

	private static Boolean parseHasBorder(String value) {
		return !value.equals("none");
	}

	private static float translateFontSize(int fontSize) {

		switch (fontSize) {
		case 1:
			return 0.6f;
		case 2:
			return 0.8f;
		case 3:
			return 1.0f;
		case 4:
			return 1.2f;
		case 5:
			return 1.4f;
		case 6:
			return 1.6f;
		case 7:
			return 1.8f;
		}

		return 1.0f;
	}

	/**
	 * Parses a border definition.
	 * 
	 * Border definitions are a complete mess, since the order is not set.
	 * 
	 * @param borderDefinition
	 * @return
	 */
	private static StyleUpdater parseBorder(String borderDefinition) {

		String[] parts = borderDefinition.split("\\s");

		StyleValue borderWidth = null;
		Integer borderColor = null;
		Style.BorderStyle borderStyle = null;

		for (String part : parts) {

			Log.d("CSSParser", "Trying to parse " + part);

			if (borderWidth == null) {

				borderWidth = StyleValue.parse(part);

				if (borderWidth != null) {
					Log.d("CSSParser", "Parsed " + part + " as border-width");
					continue;
				}
			}

			if (borderColor == null) {
				try {
					borderColor = parseCSSColor(part);
					Log.d("CSSParser", "Parsed " + part + " as border-color");
					continue;
				} catch (IllegalArgumentException ia) {
					// try next one
				}
			}

			if (borderStyle == null) {
				try {
					borderStyle = Style.BorderStyle.valueOf(part.toUpperCase());
					Log.d("CSSParser", "Parsed " + part + " as border-style");
					continue;
				} catch (IllegalArgumentException ia) {
					// next loop iteration
				}
			}

			Log.d("CSSParser", "Could not make sense of border-spec " + part);
		}

		final StyleValue finalBorderWidth = borderWidth;
		final Integer finalBorderColor = borderColor;
		final Style.BorderStyle finalBorderStyle = borderStyle;

		return new StyleUpdater() {
			@Override
			public Style updateStyle(Style style, HtmlSpanner spanner) {

				if (finalBorderColor != null) {
					style = style.setBorderColor(finalBorderColor);
				}

				if (finalBorderWidth != null) {
					style = style.setBorderWidth(finalBorderWidth);
				}

				if (finalBorderStyle != null) {
					style = style.setBorderStyle(finalBorderStyle);
				}

				return style;
			}
		};

	}

	private static StyleUpdater parseMargin(String marginValue) {

		String[] parts = marginValue.split("\\s");

		String bottomMarginString = "";
		String topMarginString = "";
		String leftMarginString = "";
		String rightMarginString = "";

		// See http://www.w3schools.com/css/css_margin.asp

		if (parts.length == 1) {
			bottomMarginString = parts[0];
			topMarginString = parts[0];
			leftMarginString = parts[0];
			rightMarginString = parts[0];
		} else if (parts.length == 2) {
			topMarginString = parts[0];
			bottomMarginString = parts[0];
			leftMarginString = parts[1];
			rightMarginString = parts[1];
		} else if (parts.length == 3) {
			topMarginString = parts[0];
			leftMarginString = parts[1];
			rightMarginString = parts[1];
			bottomMarginString = parts[2];
		} else if (parts.length == 4) {
			topMarginString = parts[0];
			rightMarginString = parts[1];
			bottomMarginString = parts[2];
			leftMarginString = parts[3];
		}

		final StyleValue marginBottom = StyleValue.parse(bottomMarginString);
		final StyleValue marginTop = StyleValue.parse(topMarginString);
		final StyleValue marginLeft = StyleValue.parse(leftMarginString);
		final StyleValue marginRight = StyleValue.parse(rightMarginString);

		return new StyleUpdater() {
			@Override
			public Style updateStyle(Style style, HtmlSpanner spanner) {
				Style resultStyle = style;

				if (marginBottom != null) {
					resultStyle = resultStyle.setMarginBottom(marginBottom);
				}

				if (marginTop != null) {
					resultStyle = resultStyle.setMarginTop(marginTop);
				}

				if (marginLeft != null) {
					resultStyle = resultStyle.setMarginLeft(marginLeft);
				}

				if (marginRight != null) {
					resultStyle = resultStyle.setMarginRight(marginRight);
				}

				return resultStyle;
			}
		};
	}

}
